Running Excalibur in React

By
utility
react

@avalon/Excalibur/Utility/ExcaliburContainer.tsx

src/registry/Excalibur/Utility/ExcaliburContainer.tsx
1
import {Actor, Engine, EngineOptions, Scene} from "excalibur";
2
import {ReactElement, useEffect, useId, useRef, useState} from "react";
3
4
const engines = new Map<string, Engine>();
5
6
const cleanup = new Map<string, number>();
7
8
export type ExcaliburOptions = Omit<EngineOptions, 'canvasElementId' | 'canvasElement'>
9
10
type ExcaliburContainerProps = {
11
className?: string,
12
scene?: string,
13
scenes?: Record<string, Scene>,
14
actors?: Actor[],
15
options?: ExcaliburOptions,
16
isRunning?: boolean,
17
}
18
19
const toggleEngine = (engine: Engine, shouldRun: boolean): void => {
20
if (shouldRun && !engine.clock.isRunning()) {
21
engine.clock.start();
22
} else if (!shouldRun && engine.clock.isRunning()) {
23
engine.once('postframe', () => engine.clock.stop());
24
}
25
};
26
27
const loadScenes = (engine: Engine, scenes: Record<string, Scene>): void => {
28
for (const sceneName of Object.keys(scenes)) {
29
if (engine.scenes[sceneName] === scenes[sceneName]) {
30
continue;
31
}
32
33
engine.addScene(sceneName, scenes[sceneName]);
34
}
35
};
36
37
const loadActors = (engine: Engine, actors: Actor[]): void => {
38
const currentScene = engine.currentScene;
39
for (const actor of actors) {
40
if (currentScene.actors.includes(actor)) {
41
continue;
42
}
43
44
currentScene.add(actor);
45
}
46
};
47
48
export const ExcaliburContainer = ({
49
options,
50
scene,
51
className,
52
scenes = {},
53
actors = [],
54
isRunning = true,
55
}: ExcaliburContainerProps): ReactElement => {
56
const instanceId = useId();
57
const canvasRef = useRef<HTMLCanvasElement>(null);
58
const [currentSceneName, setCurrentSceneName] = useState<string | undefined>(scene);
59
60
useEffect(() => {
61
const canvasElement = canvasRef.current;
62
if (!canvasElement) {
63
throw new Error('canvasRef.current should always return HTMLCanvasElement.');
64
}
65
66
if (engines.has(instanceId)) {
67
const cleanupTimeout = cleanup.get(instanceId);
68
if (cleanupTimeout !== undefined) {
69
clearTimeout(cleanupTimeout);
70
}
71
return;
72
}
73
74
canvasElement.oncontextmenu = (): boolean => false;
75
76
const engine = new Engine({
77
...options,
78
canvasElement,
79
});
80
81
loadScenes(engine, scenes);
82
83
if (scene) {
84
engine.goToScene(scene)
85
.then(() => loadActors(engine, actors))
86
.catch(() => console.error(`Unable to go to scene "${scene}`));
87
}
88
89
engine.start().catch(() => console.log('Unable to start Excalibur engine'));
90
91
engines.set(instanceId, engine);
92
93
return (): void => {
94
cleanup.set(instanceId, self.setTimeout(() => engine.stop(), 100));
95
};
96
}, [actors, instanceId, options, isRunning, scene, scenes]);
97
98
useEffect(() => {
99
const engine = engines.get(instanceId);
100
if (!engine) {
101
return;
102
}
103
104
loadScenes(engine, scenes);
105
106
loadActors(engine, actors);
107
108
if (scene && scene !== currentSceneName) {
109
if (!Object.keys(engine.scenes).includes(scene)) {
110
throw new Error(`Scene "${scene} is not loaded.`);
111
}
112
113
engine.goToScene(scene)
114
.then(() => setCurrentSceneName(engine.currentSceneName))
115
.catch(() => console.error(`Unable to go to scene "${scene}`));
116
} else {
117
toggleEngine(engine, isRunning);
118
}
119
}, [instanceId, scenes, actors, scene, currentSceneName, isRunning]);
120
121
return <canvas
122
id={instanceId}
123
ref={canvasRef}
124
className={className}
125
></canvas>;
126
};